package com.silence.gmall.template.impl;

import com.alibaba.fastjson2.JSON;
import com.silence.gmall.config.properties.MinioProperties;
import com.silence.gmall.enums.CommonCode;
import com.silence.gmall.exception.ServiceException;
import com.silence.gmall.common.vo.req.ChunkUploadReqVo;
import com.silence.gmall.common.vo.req.MergeChunksReqVo;
import com.silence.gmall.common.vo.res.ChunkUploadResVo;
import com.silence.gmall.common.vo.res.FileUploadResVo;
import com.silence.gmall.template.ChunkUploadTemplate;
import io.minio.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.io.InputStream;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

/**
 * 分片上传实现类
 *
 * @author silence
 */
@Slf4j
@Component
@RequiredArgsConstructor
@ConditionalOnBean(MinioClient.class)
public class ChunkUploadTemplateImpl implements ChunkUploadTemplate {

    private final MinioClient minioClient;
    private final MinioProperties minioProperties;
    private final StringRedisTemplate stingRedisTemplate;

    // Redis key 前缀
    private static final String CHUNK_UPLOAD_PREFIX = "CHUNK_UPLOAD:";
    private static final String CHUNK_LIST_PREFIX = "CHUNK_LIST:";

    // 分片上传信息过期时间（7天）
    private static final long UPLOAD_INFO_EXPIRE_TIME = 604800L;

    @Override
    public FileUploadResVo checkFileExists(String fileIdentifier, String fileName) {
        String objectName = buildFinalObjectName(fileName);

        try {
            // 检查文件是否已存在
            StatObjectResponse stat = minioClient.statObject(
                    StatObjectArgs.builder()
                            .bucket(minioProperties.getBucketName())
                            .object(objectName)
                            .build()
            );

            // 文件已存在，返回文件信息（秒传）
            String fileUrl = String.format("%s/%s/%s",
                    minioProperties.getFileHost(),
                    minioProperties.getBucketName(),
                    objectName);

            log.info("文件秒传成功：{}", fileName);

            return FileUploadResVo.builder()
                    .fileName(extractFileName(objectName))
                    .originalFileName(fileName)
                    .fileSize(stat.size())
                    .contentType(stat.contentType())
                    .fileUrl(fileUrl)
                    .bucketName(minioProperties.getBucketName())
                    .objectName(objectName)
                    .uploadTime(System.currentTimeMillis())
                    .build();

        } catch (Exception _) {
            // 文件不存在，正常情况
            log.debug("文件不存在，需要上传：{}", fileName);
            return null;
        }
    }

    @Override
    public ChunkUploadResVo checkChunkStatus(String fileIdentifier) {
        String uploadInfoKey = CHUNK_UPLOAD_PREFIX + fileIdentifier;
        String chunkListKey = CHUNK_LIST_PREFIX + fileIdentifier;

        String uploadInfoJson = stingRedisTemplate.opsForValue().get(uploadInfoKey);
        if (uploadInfoJson == null) {
            return ChunkUploadResVo.builder()
                    .fileIdentifier(fileIdentifier)
                    .completed(false)
                    .uploadedChunks(new ArrayList<>())
                    .progress(0.0)
                    .message("未找到上传记录")
                    .build();
        }

        // 获取已上传的分片列表
        Set<String> chunkIndexes = stingRedisTemplate.opsForSet().members(chunkListKey);
        List<Integer> uploadedChunks = chunkIndexes != null ?
                chunkIndexes.stream()
                        .map(Integer::parseInt)
                        .sorted()
                        .toList() : new ArrayList<>();

        // 解析上传信息
        Map<String, Object> uploadInfo = JSON.parseObject(uploadInfoJson);
        Integer totalChunks = (Integer) uploadInfo.get("totalChunks");

        // 计算进度
        double progress = totalChunks > 0 ?
                (double) uploadedChunks.size() / totalChunks * 100 : 0.0;

        boolean completed = uploadedChunks.size() == totalChunks;

        return ChunkUploadResVo.builder()
                .fileIdentifier(fileIdentifier)
                .completed(completed)
                .uploadedChunks(uploadedChunks)
                .progress(progress)
                .message(completed ? "上传完成，等待合并" : "上传中")
                .build();
    }

    @Override
    public ChunkUploadResVo uploadChunk(ChunkUploadReqVo request) {
        String fileIdentifier = request.getFileIdentifier();
        String uploadInfoKey = CHUNK_UPLOAD_PREFIX + fileIdentifier;
        String chunkListKey = CHUNK_LIST_PREFIX + fileIdentifier;

        try {
            // 保存上传信息
            Map<String, Object> uploadInfo = new HashMap<>();
            uploadInfo.put("fileName", request.getFileName());
            uploadInfo.put("totalSize", request.getTotalSize());
            uploadInfo.put("totalChunks", request.getTotalChunks());
            uploadInfo.put("chunkSize", request.getChunkSize());
            uploadInfo.put("contentType", request.getContentType());
            uploadInfo.put("createTime", System.currentTimeMillis());

            stingRedisTemplate.opsForValue().set(uploadInfoKey, JSON.toJSONString(uploadInfo),
                    UPLOAD_INFO_EXPIRE_TIME, TimeUnit.SECONDS);

            // 检查分片是否已上传
            String chunkIndexStr = String.valueOf(request.getChunkIndex());
            if (Boolean.TRUE.equals(stingRedisTemplate.opsForSet().isMember(chunkListKey, chunkIndexStr))) {
                log.info("分片 {} 已存在，跳过上传", request.getChunkIndex());
            } else {
                // 上传分片到 MinIO
                String chunkObjectName = buildChunkObjectName(fileIdentifier, request.getChunkIndex());

                try (InputStream inputStream = request.getChunk().getInputStream()) {
                    minioClient.putObject(
                            PutObjectArgs.builder()
                                    .bucket(minioProperties.getBucketName())
                                    .object(chunkObjectName)
                                    .stream(inputStream, request.getCurrentChunkSize(), -1)
                                    .contentType("application/octet-stream")
                                    .build()
                    );
                }

                // 记录已上传的分片
                stingRedisTemplate.opsForSet().add(chunkListKey, chunkIndexStr);
                stingRedisTemplate.expire(chunkListKey, UPLOAD_INFO_EXPIRE_TIME, TimeUnit.SECONDS);

                log.info("分片上传成功：{}/{}", request.getChunkIndex(), request.getTotalChunks());
            }

            // 返回当前状态
            return checkChunkStatus(fileIdentifier);

        } catch (Exception e) {
            log.error("分片上传失败：{}", e.getMessage(), e);
            throw new ServiceException(CommonCode.FAIL, "分片上传失败：" + e.getMessage());
        }
    }

    @Override
    public FileUploadResVo mergeChunks(MergeChunksReqVo request) {
        String fileIdentifier = request.getFileIdentifier();
        String chunkListKey = CHUNK_LIST_PREFIX + fileIdentifier;

        try {
            // 检查所有分片是否都已上传
            Set<String> uploadedChunks = stingRedisTemplate.opsForSet().members(chunkListKey);
            if (uploadedChunks == null || uploadedChunks.size() != request.getTotalChunks()) {
                throw new ServiceException(CommonCode.FAIL,
                        String.format("分片不完整，已上传：%d，总数：%d",
                                uploadedChunks != null ? uploadedChunks.size() : 0,
                                request.getTotalChunks()));
            }

            // 按顺序合并分片
            String finalObjectName = buildFinalObjectName(request.getFileName());

            // 使用 MinIO 的 Compose 功能合并分片
            List<ComposeSource> sources = IntStream.range(0, request.getTotalChunks())
                    .mapToObj(i -> ComposeSource.builder()
                            .bucket(minioProperties.getBucketName())
                            .object(buildChunkObjectName(fileIdentifier, i))
                            .build())
                    .toList();

            minioClient.composeObject(
                    ComposeObjectArgs.builder()
                            .bucket(minioProperties.getBucketName())
                            .object(finalObjectName)
                            .sources(sources)
                            .build()
            );

            // 删除临时分片文件
            cleanupChunks(fileIdentifier, request.getTotalChunks());

            // 清理 Redis 缓存
            stingRedisTemplate.delete(CHUNK_UPLOAD_PREFIX + fileIdentifier);
            stingRedisTemplate.delete(chunkListKey);

            // 构建文件访问URL
            String fileUrl = String.format("%s/%s/%s",
                    minioProperties.getFileHost(),
                    minioProperties.getBucketName(),
                    finalObjectName);

            log.info("文件合并成功：{} -> {}", request.getFileName(), finalObjectName);

            return FileUploadResVo.builder()
                    .fileName(extractFileName(finalObjectName))
                    .originalFileName(request.getFileName())
                    .fileSize(request.getTotalSize())
                    .contentType(request.getContentType())
                    .fileUrl(fileUrl)
                    .bucketName(minioProperties.getBucketName())
                    .objectName(finalObjectName)
                    .uploadTime(System.currentTimeMillis())
                    .build();

        } catch (Exception e) {
            log.error("文件合并失败：{}", e.getMessage(), e);
            throw new ServiceException(CommonCode.FAIL, "文件合并失败：" + e.getMessage());
        }
    }

    @Override
    public void cancelUpload(String fileIdentifier) {
        try {
            String uploadInfoKey = CHUNK_UPLOAD_PREFIX + fileIdentifier;
            String chunkListKey = CHUNK_LIST_PREFIX + fileIdentifier;

            // 获取上传信息
            String uploadInfoJson = stingRedisTemplate.opsForValue().get(uploadInfoKey);
            if (uploadInfoJson != null) {
                Map<String, Object> uploadInfo = JSON.parseObject(uploadInfoJson);
                Integer totalChunks = (Integer) uploadInfo.get("totalChunks");

                // 删除所有分片文件
                if (totalChunks != null) {
                    cleanupChunks(fileIdentifier, totalChunks);
                }
            }

            // 清理 Redis 缓存
            stingRedisTemplate.delete(uploadInfoKey);
            stingRedisTemplate.delete(chunkListKey);

            log.info("取消上传并清理临时文件：{}", fileIdentifier);

        } catch (Exception e) {
            log.error("取消上传失败：{}", e.getMessage(), e);
        }
    }

    /**
     * 清理分片文件
     */
    private void cleanupChunks(String fileIdentifier, int totalChunks) {
        for (int i = 0; i < totalChunks; i++) {
            try {
                String chunkObjectName = buildChunkObjectName(fileIdentifier, i);
                minioClient.removeObject(
                        RemoveObjectArgs.builder()
                                .bucket(minioProperties.getBucketName())
                                .object(chunkObjectName)
                                .build()
                );
            } catch (Exception e) {
                log.warn("删除分片文件失败：{}", e.getMessage());
            }
        }
    }

    /**
     * 构建分片对象名
     */
    private String buildChunkObjectName(String fileIdentifier, int chunkIndex) {
        return String.format("chunks/%s/%d", fileIdentifier, chunkIndex);
    }

    /**
     * 构建最终文件对象名
     */
    private String buildFinalObjectName(String fileName) {
        String dateDir = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
        String uuid = UUID.randomUUID().toString().replace("-", "");
        String extension = getFileExtension(fileName);
        return String.format("%s/%s%s", dateDir, uuid, extension);
    }

    /**
     * 获取文件扩展名
     */
    private String getFileExtension(String fileName) {
        if (fileName == null || fileName.isEmpty()) {
            return "";
        }
        int lastDotIndex = fileName.lastIndexOf('.');
        return lastDotIndex > 0 ? fileName.substring(lastDotIndex) : "";
    }

    /**
     * 从对象名中提取文件名
     */
    private String extractFileName(String objectName) {
        if (objectName == null || objectName.isEmpty()) {
            return "";
        }
        return objectName.substring(objectName.lastIndexOf('/') + 1);
    }
}